---
title: "Console API Migration Plan"
description: "Mapping old Developer Console routes to the new /api/console surface, with middleware, services, rollout, and frontend (Zustand) notes."
---

# Console API Migration Plan

This document tracks the migration of Developer Console functionality to an audience-scoped API and service layer. We will:
- Keep all legacy routes in place during migration (no breaking changes).
- Introduce new, well-scoped `/api/console/*` routes and services.
- Migrate the Console UI (websites/console) to the new routes gradually.
- After full rollout, remove legacy routes and services in a separate PR.

Goals:
- Make org context explicit (path param), not magical headers.
- Remove side effects from middleware (e.g., “auto-create org”).
- Centralize business logic in console services.
- Enforce clear auth and role checks.
- Improve developer experience and maintainability.

---

## Principles & Conventions

- One resource per file under `src/api/console/*` with a single default `router` export (e.g., `console.app.api.ts`, `org.api.ts`).
- Short header in each API file listing base path and endpoints.
- Routes at the top, handler functions below (hoisted).
- Per-route middleware (no global `router.use`).
- Authentication middleware attaches `req.console` with `{ email }` only (no DB calls).
- Org context is explicit in URLs for org-scoped resources (e.g., `/api/console/orgs/:orgId/...`); app routes use a flat namespace.
- No side effects in authentication middleware (no implicit org creation).
- Sensitive values (API keys/secrets) are never logged or returned after creation.

---

## Current State: Legacy Endpoints (in use by Developer Console)

Authentication
- GET /api/dev/auth/me
- PUT /api/dev/auth/profile (deprecated)

Organizations
- GET /api/orgs
- POST /api/orgs
- GET /api/orgs/:orgId
- PUT /api/orgs/:orgId
- DELETE /api/orgs/:orgId
- POST /api/orgs/:orgId/members
- PATCH /api/orgs/:orgId/members/:memberId
- DELETE /api/orgs/:orgId/members/:memberId
- POST /api/orgs/accept/:token
- POST /api/orgs/:orgId/invites/resend
- POST /api/orgs/:orgId/invites/rescind

Apps (Developer Console)
- GET /api/dev/apps
- POST /api/dev/apps/register
- GET /api/dev/apps/:packageName
- PUT /api/dev/apps/:packageName
- DELETE /api/dev/apps/:packageName
- POST /api/dev/apps/:packageName/api-key
- GET /api/dev/apps/:packageName/share
- POST /api/dev/apps/:packageName/share
- POST /api/dev/apps/:packageName/publish
- PATCH /api/dev/apps/:packageName/visibility (deprecated)
- PATCH /api/dev/apps/:packageName/share-emails (deprecated)
- POST /api/dev/apps/:packageName/move-org

Assets (Cloudflare Images)
- POST /api/dev/images/upload
- DELETE /api/dev/images/:imageId

Permissions
- GET /api/permissions/:packageName
- PATCH /api/permissions/:packageName

Admin panel (not org-scoped; out of scope for this migration)

---

## Problems Identified

- Organization context is magical and implicit:
  - Middleware attempts x-org-id header → defaultOrg → “sync orphaned memberships” → create personal org.
  - This hides required context and creates DB side effects during auth.
- Multiple org header names are probed (x-org-id, x-organization-id, organization-id).
- Some routes likely lack explicit role checks (e.g., image delete).
- Permissions routes need clear org membership/role guardrails.
- Developer UX suffers when org resolution fails—users can’t create apps.

---

## New API Surface: /api/console (Target)

Auth
- GET /api/console/auth/me
  - Returns user, organizations, defaultOrgId
  - Optional: if the user has zero orgs, create a “personal org” here (configurable, explicit)

Organizations
- GET /api/console/orgs
- POST /api/console/orgs
- GET /api/console/orgs/:orgId
- PUT /api/console/orgs/:orgId
- DELETE /api/console/orgs/:orgId
- POST /api/console/orgs/:orgId/members
- PATCH /api/console/orgs/:orgId/members/:memberId
- DELETE /api/console/orgs/:orgId/members/:memberId
- POST /api/console/orgs/accept/:token
- POST /api/console/orgs/:orgId/invites/resend
- POST /api/console/orgs/:orgId/invites/rescind

Apps
- Apps (flat namespace)
- GET /api/console/apps?orgId=...          (optional org filter; if omitted, return all user-accessible apps)
- POST /api/console/apps                    (body can include orgId; if missing, use defaultOrg or bootstrap personal org)
- GET /api/console/app/:packageName
- PUT /api/console/app/:packageName
- DELETE /api/console/app/:packageName
- POST /api/console/app/:packageName/api-key      (admin; body can include orgId; else defaultOrg/bootstrap)
- POST /api/console/app/:packageName/publish      (admin; body can include orgId; else defaultOrg/bootstrap)
- POST /api/console/app/:packageName/move         (body: { targetOrgId })
- GET /api/console/app/:packageName/share
- POST /api/console/app/:packageName/share
- GET /api/console/app/:packageName/permissions
- PATCH /api/console/app/:packageName/permissions

Assets
- POST /api/console/orgs/:orgId/assets/images
- DELETE /api/console/orgs/:orgId/assets/images/:imageId

Notes:
- Org context is explicit in the URL.
- For transition, we can keep reading x-org-id when :orgId is not present, but new routes will require :orgId.
- Role checks:
  - Member: read operations
  - Admin: write operations (create/update/delete app, regen API key, manage org/members, image delete, etc.)

---

## Route Crosswalk (Legacy → New)

Authentication
- GET /api/dev/auth/me → GET /api/console/auth/me
- PUT /api/dev/auth/profile (deprecated) → n/a (update org profile via org routes)

Organizations
- /api/orgs (GET, POST) → /api/console/orgs
- /api/orgs/:orgId (GET, PUT, DELETE) → /api/console/orgs/:orgId
- /api/orgs/:orgId/members (POST) → /api/console/orgs/:orgId/members (POST)
- /api/orgs/:orgId/members/:memberId (PATCH, DELETE) → same under /api/console
- /api/orgs/accept/:token → /api/console/orgs/accept/:token
- /api/orgs/:orgId/invites/resend → /api/console/orgs/:orgId/invites/resend
- /api/orgs/:orgId/invites/rescind → /api/console/orgs/:orgId/invites/rescind

Apps
- GET /api/dev/apps → GET /api/console/orgs/:orgId/apps
- POST /api/dev/apps/register → POST /api/console/orgs/:orgId/apps
- GET /api/dev/apps/:packageName → GET /api/console/orgs/:orgId/apps/:packageName
- PUT /api/dev/apps/:packageName → PUT /api/console/orgs/:orgId/apps/:packageName
- DELETE /api/dev/apps/:packageName → DELETE /api/console/orgs/:orgId/apps/:packageName
- POST /api/dev/apps/:packageName/api-key → POST /api/console/orgs/:orgId/apps/:packageName/api-key
- GET /api/dev/apps/:packageName/share → GET /api/console/orgs/:orgId/apps/:packageName/share
- POST /api/dev/apps/:packageName/share → POST /api/console/orgs/:orgId/apps/:packageName/share
- POST /api/dev/apps/:packageName/publish → POST /api/console/orgs/:orgId/apps/:packageName/publish
- PATCH /api/dev/apps/:packageName/visibility (deprecated) → n/a
- PATCH /api/dev/apps/:packageName/share-emails (deprecated) → n/a
- POST /api/dev/apps/:packageName/move-org → POST /api/console/orgs/:orgId/apps/:packageName/move

Assets
- POST /api/dev/images/upload → POST /api/console/orgs/:orgId/assets/images
- DELETE /api/dev/images/:imageId → DELETE /api/console/orgs/:orgId/assets/images/:imageId

Permissions
- GET /api/permissions/:packageName → GET /api/console/orgs/:orgId/apps/:packageName/permissions
- PATCH /api/permissions/:packageName → PATCH /api/console/orgs/:orgId/apps/:packageName/permissions

---

## Middleware (Old vs New)

Old
- validateSupabaseToken (actually verifies an AugmentOS JWT)
  - Parses multiple org headers (x-org-id, x-organization-id, organization-id)
  - If no header: uses user.defaultOrg from DB
  - If still missing: attempts to “sync memberships” or creates a personal org
  - Attaches `developerEmail` and `currentOrgId` to req
  - Side-effects (DB writes) in auth middleware cause surprises and race conditions

New (Proposed)
- authWithConsole
  - Verifies JWT and attaches `req.console` with `{ email }`
  - No DB calls; no org parsing in middleware
- No org parsing in middleware
  - Handlers pass explicit inputs to services; services resolve org (and may bootstrap for a small, allowed set of write endpoints)
- Role checks
  - Enforced by services (or handler-level guards) after org resolution
- auditLog / rateLimit (optional additions)
- errorHandler (standardized JSON errors)

Org bootstrap (explicit)
- Decide one place to ensure a personal org for a new user:
  - Option A: within GET /api/console/auth/me when user has zero orgs
  - Option B: dedicated /api/console/orgs/bootstrap (POST) called by UI

---

## Services (Console Audience)

Create new services under `src/services/console/`:

- console.app.service.ts
  - CRUD for apps, ownership checks (org-scoped), publishing
  - API key lifecycle (generate once, return plaintext once; store hash)
  - Permissions get/update
  - Move app between orgs (admin in both orgs)
- org.service.ts
  - Org CRUD, invites, membership, roles, defaultOrg management
  - Personal org creation for new users
- assets.service.ts
  - Image upload/delete; Cloudflare integration; org authorization
- validation.service.ts
  - Canonical tool/settings validation (no drift)
- publishing.service.ts
  - Publish workflow, review notes, statuses, audit history

Notes:
- Reuse core models; keep SDK auth concerns separate from console routes.
- Scrub sensitive values from logs (API keys, endpoint secrets).
- Add indexes for org-scoped lookups (apps by orgId, etc.).

---

## Security Notes

- No plaintext API keys beyond creation response.
- Compare hashes with constant-time equality.
- Rate-limit sensitive console endpoints (API key regen, publishing).
- Return 409 when org context is missing; don’t mutate state in middleware.
- Use role-based guards for all write operations.

---

## Rollout & Deprecation

Phase 1 (Additive)
- Ship `/api/console/*` routes and services (no behavior changes yet).
- Console UI begins migrating to new routes; continue to send `x-org-id` temporarily if needed.

Phase 2 (Migration)
- Flip Console UI to path-scoped `:orgId` exclusively.
- Remove header fallback from `resolveOrg` (or behind a feature flag).

Phase 3 (Cleanup)
- Remove legacy `/api/dev/*` and `/api/orgs/*` usage from the UI.
- PR to delete old routes and related legacy pathways in the backend.

---

## Frontend: Console Migration & Zustand Stores

State Management (Zustand)
- account.store
  - State: email, signedIn, loading, error
  - Actions: setToken(), fetchMe(), signOut()
  - Notes: Cloud ensures a default org (on auth and/or org-required actions)

- org.store
  - State: orgs, selectedOrgId, loading, error
  - Derived: selectedOrg
  - Actions: bootstrap(), loadOrgs(), setSelectedOrgId(orgId), createOrg(name)
  - Behavior: when selectedOrgId changes, set axios.defaults.headers["x-org-id"] and persist to localStorage key "console:selectedOrgId"

- app.store
  - State: appsByPackage, list (packageName[]), loading, error, lastFetchedAt
  - Actions: fetchApps(), getApp(), createApp(), updateApp(), deleteApp(), publishApp(), regenerateApiKey(), moveApp()
  - Notes: uses selected org from org.store; refetch on org change or when views mount

Initialization
- After token is set:
  - account.fetchMe()
  - org.bootstrap()
  - app.fetchApps()

Temporary compatibility
- While legacy routes are still in use, org.store sets axios.defaults.headers["x-org-id"] globally when the selected org changes.
- New /api/console routes will not rely on this header; it’s a bridge until migration completes.

Persistence
- org.store persists the selected org id using the key "console:selectedOrgId".

Transport & Auth
- Prefer a single API client with baseURL and Authorization header.
- Pass `:orgId` in the route (preferred). During transition, keep `x-org-id` until all calls are moved.

State Management (Zustand)
- authStore
  - user, token, organizations[], defaultOrgId, loading, error
  - actions: fetchMe(), setToken(), setDefaultOrg()
- orgStore
  - selectedOrgId, orgsById
  - actions: listOrgs(), createOrg(), getOrg(), updateOrg(), deleteOrg(), members/invites actions
- appsStore
  - appsByOrg: Record<orgId, App[]>, loading flags, lastFetchedAt
  - actions: listApps(orgId), createApp(orgId, data), getApp(orgId, pkg), updateApp(orgId, pkg, data), deleteApp(orgId, pkg), publishApp(orgId, pkg), moveApp(orgId, pkg, targetOrgId), regenApiKey(orgId, pkg)
- permissionsStore
  - byApp: Record<packageName, Permission[]>
  - actions: getPermissions(orgId, pkg), updatePermissions(orgId, pkg, permissions)
- assetsStore
  - uploads state, imageById, errors
  - actions: uploadImage(orgId, file, metadata), deleteImage(orgId, imageId)
- uiStore (optional)
  - toasts, dialogs, global busy indicators

Guidelines
- Co-locate API calls within store actions to keep components lean.
- Use selectors for render-performance.
- Consider `persist` or `immer` middleware as needed.
- Normalize by IDs where helpful (apps map by packageName for quick updates).

---

## Migration Checklist (Initial)

Backend
- [ ] Add `/api/console/auth/me` (no side effects except optional personal org bootstrap)
- [ ] Add `/api/console/orgs/*` routes + `org.service.ts`
- [ ] Add `/api/console/orgs/:orgId/apps*` routes + `apps.service.ts`
- [ ] Add `/api/console/orgs/:orgId/assets/images*` routes + `assets.service.ts`
- [ ] Add `/api/console/orgs/:orgId/apps/:packageName/permissions*` + `validation.service.ts`
- [ ] Implement `authWithUser`, `resolveOrg`, `requireRole`
- [ ] Document org context precedence and 409 errors
- [ ] Add rate limiting on console routes
- [ ] Logging scrub (no secrets)

Frontend (websites/console)
- [ ] Introduce Zustand stores (auth, orgs, apps, permissions, assets, ui)
- [ ] Update API client to favor `/api/console/*` with path `:orgId`
- [ ] Add “No organization” UX: prompt to create/select org if 409 received
- [ ] Migrate screens feature-by-feature (orgs → apps → permissions → assets)
- [ ] Remove `x-org-id` usages after path migration completes

Docs
- [ ] Add `/cloud/docs/console/*` pages:
  - overview, auth, orgs, apps, permissions, assets, publishing
- [ ] Deprecation notice: `/api/dev/*` and `/api/orgs/*` (timeline, migration steps)
- [ ] Update any internal references to org header behavior

---

## Open Questions

- Should personal org creation happen in `GET /api/console/auth/me` (automatic) or via an explicit bootstrap route/button?
- Do we need a feature flag for header fallback (x-org-id) during transition?
- Where should we surface usage metrics & logs for developers (separate analytics service vs inline endpoints)?
- Ownership tags per service/router for traceability?

---

## Appendix: Error Semantics

- 401 Unauthorized: missing/invalid Authorization header/token
- 403 Forbidden: user lacks role in the org
- 404 Not Found: resource does not exist (org/app)
- 409 Conflict: missing organization context (no :orgId and no header fallback; or user has zero orgs)
- 422 Unprocessable Entity: validation failed (tool/settings)
- 500 Internal Server Error: unexpected failures

---

This document is the living source for the console migration. As we implement pieces, keep the crosswalk and checklists updated, and record any deviations or decisions here.

---

## Org Bootstrap Semantics

When an operation requires an organization context but no `orgId` is provided and the user has no `defaultOrg`, a personal organization may be auto-bootstrapped in a small, controlled set of write endpoints. Reads never auto-bootstrap; they should return a helpful error.

Resolution precedence for org context:
1) Explicit org in payload: `body.orgId` (write operations)  
2) Optional query param: `?orgId=` (primarily for read/list operations)  
3) `user.defaultOrg` (if set)  
4) If still missing and the endpoint is in the bootstrap-allowed list below, create a personal org for the user and set as `defaultOrg` for future requests. Otherwise, return `409 Conflict` with guidance to create/select an org.

Endpoints that MAY auto-bootstrap a personal org (write ops only):
- POST /api/console/apps
  - Create a new app; if `orgId` is not provided, use `defaultOrg`. If none exists, bootstrap a personal org, then create.
- POST /api/console/app/:packageName/api-key
  - Regenerate API key; if `orgId` is not provided, resolve via precedence. If none exists, bootstrap and proceed (admin check still applies).
- POST /api/console/app/:packageName/publish
  - Publish app; if org cannot be resolved, bootstrap, then apply admin guard.
- POST /api/console/app/:packageName/move
  - Source org should be resolvable from ownership; target org must be explicit in `body.targetOrgId`. If source cannot be resolved and user has no orgs at all, bootstrap before validating permissions.
- POST /api/console/orgs/:orgId/assets/images
  - Preferred: explicit `:orgId` in path. If we also support a flat variant in the future (e.g., POST /api/console/assets/images), then use precedence and bootstrap if missing (admin guard applies).

Endpoints that DO NOT bootstrap (return 409 if missing org):
- GET /api/console/apps?orgId=...
  - If `orgId` omitted, returns all user-accessible apps (no bootstrap needed). If an org-specific view is requested and not resolvable, return 409 with guidance.
- GET /api/console/app/:packageName
  - If ownership/org context cannot be derived from package ownership, return 409 with guidance.
- Any read-only org-scoped routes (e.g., org details, members list) that cannot resolve org context and do not accept `?orgId=...`.

Operational notes:
- Auto-bootstrap should be logged (audit trail) and rate-limited to prevent abuse.
- Auto-bootstrap should occur at most once per user (idempotent behavior).
- Admin-only routes still require role checks after bootstrap (user will be admin of their personal org).
- Return structured error payloads for 409 responses to guide UI (e.g., "No organization found. Create a personal org?").